package pacman;

import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Timer;
import java.util.TimerTask;

import maze.Barcode;
import maze.Coordinate;
import maze.Location;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
import communication.Config;

/**
 * A class representing a game of pacman with 4 ghosts and 1 pacman
 */
public class Game {
	Location[] robotLocations = { new Location(140, 60), new Location(60, 60), new Location(100, 60), new Location(20, 140) };
	/**
	 * The players of this game controlling the ghosts
	 */
	private Ghost[] ghosts;
	
	/**
	 * The pacman the players will chase
	 */
	private Pacman pacman;
	
	/**
	 * Indicates the number of JOIN commands received
	 */
	private int numberOfJoins;
	
	/**
	 * The number of players that have registerd their name
	 */
	private int numberOfNamesRegistered;
	
	/**
	 * The last locations where Pacman was spotted.
	 */
	private ArrayList<Coordinate> lastPacmanLocations = new ArrayList<Coordinate>();
	
	/**
	 * The channel used for communicating
	 */
	private Channel channel;
	
	private QueueingConsumer consumer;
	 
	private CommandParser parser;
	
	private Timer timer;
	
	private HashMap<Ghost,Coordinate> ghostPositions = new HashMap<Ghost,Coordinate>();
	
	//Always make the game before making the ghosts!
	public Game() throws IOException {
		//Ghost[] tempghosts = {new Ghost(new Location(20,20),0,"Jos"),new Ghost(new Location(20,20),0,"Jef"),new Ghost(new Location(20,20),0,"Jan"),new Ghost(new Location(20,20),0,"Jeroen")};
		//setGhosts(tempghosts);
		this.ghosts= new Ghost[4];
		setPacman(new Pacman());
		setNumberOfJoins(0);	
		setNumberOfNamesRegistered(0);
		//int[][] temppositions = {{0,0},{0,0},{0,0},{0,0}};
		//positions=temppositions;
		
		setUpRabbitMQ();

        getChannel().exchangeDeclare(Config.EXCHANGE_NAME, "fanout");
        String queueName = getChannel().queueDeclare().getQueue();
        getChannel().queueBind(queueName, Config.EXCHANGE_NAME, "");

        System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
        
        setConsumer(new QueueingConsumer(getChannel()));
        
        this.channel.basicConsume(queueName, true, getConsumer());
        
        setParser(new CommandParser(this));
		this.setTimer(new Timer());
		
		ReceiveTask receiveCommand = new ReceiveTask();
		//Om de 100ms alle ingekomen messages verwerken
		getTimer().schedule(receiveCommand, 10);
	}
	
	private class ReceiveTask extends TimerTask{
		
		public ReceiveTask(){
		}
		
		@Override
		public void run() {
			while(true){
				try {
					QueueingConsumer.Delivery delivery = getConsumer().nextDelivery();
					String message = new String(delivery.getBody());
					System.out.println("Message received: "+ message);
					getParser().parseCommand(message);
				} catch (Exception e) {
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
			}
		}
	}
	
	//Zelfde staat in ghost
	private void setUpRabbitMQ() throws IOException {
		ConnectionFactory factory = new ConnectionFactory();
		factory.setHost(Config.HOST_NAME);
		Connection connection = factory.newConnection();
		setChannel(connection.createChannel());
	}

	public void addGhost(Ghost ghost) {
		for (int i = 0; i < getGhosts().length; i++) {
			if (getGhosts()[i] == null) {
				getGhosts()[i] = ghost;
				return;
			}
		}
		System.err.println("Teveel ghosts willen toevoegen");
	}

	public boolean isInitialized() {
		return getNumberOfNamesRegistered() == 4;
	}
	
	public boolean stayInJoinPhase() {
		return (getNumberOfJoins() < 4 && getNumberOfNamesRegistered() == 0);
	}

	/**
	 * Processes the JOIN command
	 * @throws IOException 
	 */
	public void processJOIN() throws IOException{
		int index = getNumberOfJoins();
		getGhosts()[index] = new Ghost(robotLocations[index], 0, "Robot" + index, this);
		getGhostPositions().put(getGhosts()[index], new Coordinate(0, 0));
		setNumberOfJoins(getNumberOfJoins()+1);
		System.out.println("aantal joins:" + getNumberOfJoins());
//		if(getNumberOfJoins() == 4 || getNumberOfNamedGhosts() != 0){
//			//Ghost met index 0 is degene die eerst werd toegevoegd
//			//dus de "eigen" robot. Enkel deze moet zijn naam sturen.
//			if(!getGhosts()[0].hasSendName()){
//				System.out.println(getGhosts()[0].getName()+ "verzend zijn naam in game:");
//				getGhosts()[0].sendNAME();
//			}else{
//				//NOP: eigen naam geen 2 keer doorsturen
//			}
//		}
	}
	
	private int getNumberOfNamedGhosts(){
		int numberOfNames = 0;
		for(Ghost ghost: getGhosts()){
			if(ghost != null && ghost.hasSendName()){
				numberOfNames++;
			}
		}
		return numberOfNames;
	}
		
	/**
	 * Processes the NAME command
	 * @throws IOException
	 * 
	 * TODO: moet kunnen verzonden worden bij manuele override van gewone voorwaarde.
	 */
	public void processNAME(String name, String version) throws IOException{
		while(getNumberOfJoins()<4){ //De robot ontving al een naam voordat hij alle joins ontving -> eerst andere ghosts initialiseren
			getGhosts()[getNumberOfJoins()] = new Ghost(robotLocations[getNumberOfJoins()], 0, "Robot" + getNumberOfJoins(), this);
			numberOfJoins++;
		}
		int index = getNumberOfNamesRegistered();
		getGhosts()[index].setName(name);
		getGhosts()[index].setVersion(version);
		//Uncomment when using testFourSimulators()
//		for(Ghost g: getOtherRobotsThan(name)){
//			g.addOtherRobotMaze(name);
//		}
		//Comment out when using TestFourSimulators()
		this.setOtherMazes(getGhosts());
		getGhosts()[index].setHasSendName(true);
		setNumberOfNamesRegistered(getNumberOfNamesRegistered()+1);
		//Eigen naam nog niet verstuurd? -> ook verzenden
		if(!getGhosts()[0].hasSendName()){
			System.out.println(getGhosts()[0].getName()+ "verzend zijn naam in game:");
			getGhosts()[0].sendNAME();
		}
	}
	
	public ArrayList<Ghost> getOtherRobotsThan(String name){
		ArrayList<Ghost> list = new ArrayList<Ghost>();
		for(Ghost g: getGhosts()){
			if(!g.getName().equals(name)) list.add(g);
		}
		return list;
	}
	
	/**
	 * Processes the POSITION command
	 */
	public void processPOSITION(String name, int x, int y){
		getGhostPositions().put(getGhostWithName(name), new Coordinate(x,y));
	}
	

	/**
	 * Processes the DISCOVER command
	 */
	public void processDISCOVER(String name, int x, int y, int north, int east, int south, int west){
		getGhostPositions().put(getGhostWithName(name), new Coordinate(x,y));
		Ghost ghost = getGhostWithName(name);
		for(int i=0; i<ghosts.length; i++){
				Ghost receivingGhost = ghosts[i];
				if(!name.equals(receivingGhost.getName())){
					Barcode barcodeRec = controlBarcode(receivingGhost, ghost);
					Coordinate coord = new Coordinate(x,y);
					receivingGhost.processDISCOVER(coord, 2-north, 2-east, 2-south, 2-west, true, name);
					if(barcodeRec != null){
						Barcode barcodeSend = ghost.getMaze().getBarcodeOnString(barcodeRec.getCode());
						if(receivingGhost.getAllMazes().get(name).match(barcodeRec.getX(), barcodeRec.getY(), barcodeSend.getX(), barcodeSend.getY(), barcodeRec.getInDirection(), barcodeSend.getInDirection())== true){
							receivingGhost.addMatchingMaze(receivingGhost.getAllMazes().get(name));
							//ghost.addMatchingMaze(ghost.getAllMazes().get(receivingGhost.getName()));
							//receivingGhost.getAllMazes().get(name).match(barcodeRec.getX(), barcodeRec.getY(), barcodeSend.getX(), barcodeSend.getY(), barcodeRec.getInDirection(), barcodeSend.getInDirection());
							// De omzetting van de doorgestuurde waarden en de waarden voor muren in onze maze...
						}
					}
						
				 }
			}

		//TODO als de absolute maze gekend is deze ook updaten
	}
	/**
	 * controleer of er 2 robots zijn met dezelfde barcode.
	 * @param receiver
	 * @param sender
	 * @return 	null als er geen dezelfde barcodes zijn
	 * 			barcode als er dezelfde barcode is.
	 */
	private Barcode controlBarcode(Ghost receiver, Ghost sender){
		try{
			for(int i=0; i< sender.getMaze().getBarcodes().size(); i++){
				Barcode barcodeSender = sender.getMaze().getBarcodes().get(i);
				for(int j=0; j < receiver.getMaze().getBarcodes().size(); j++){
					Barcode barcodeReceiver = receiver.getMaze().getBarcodes().get(j);
					if(barcodeSender.equals(barcodeReceiver)){
						return barcodeReceiver;
					}
				}
			}
		}
				catch(NullPointerException e){
					System.out.println("nullpointer");
				}	
		return null;
	}
	
	/**
	 * Een binaire barcode van de vorm abcdef (waarbij elke letter een 1 of een
	 * 0 voorstelt) wordt voorgesteld door het gehele getal
	 * 2^5a+2^4b+2^3c+2^2d+2^1e+2^0f.
	 */
	public void processBARCODEAT(String name, Coordinate co, int barcode, int direction){
		String bc = Integer.toBinaryString(barcode*2);
		while (bc.length() < 8){
			bc = 0 + bc;
		}
		if(direction == 3 || direction == 2){ //String reversen als ze niet in de richten zijn 
			StringBuffer reverser = new StringBuffer(bc);   //Die overeenkomt zoals nodig voor addBarcode
			bc = new String(reverser.reverse());
		}
		Ghost ghost = getGhostWithName(name);		
//		Coordinate co = ghost.getMaze().getCoordinateOfLocation(ghost.getLocation());
		
		if (direction == 1 || direction == 3){ //Strings zijn al gereversed dus voor verticale richtingen
			ghost.getMaze().addBarcode(co.getX(), co.getY(), bc, 1);
		}else{
			ghost.getMaze().addBarcode(co.getX(), co.getY(), bc, 0);
		}
	}
	
	/**
	 * Processes the PACMAN command by adding the location to the list of all locations where pacman has been seen.
	 */
	public void processPACMAN(int x, int y){
		getLastPacmanLocations().add(new Coordinate(x, y ));
	}
	
	/**
	 * Processes the CAPTURED command
	 */
	public void processCAPTURED(String name){
		getGhostWithName(name).processCAPTURED();
	}
	

	/**
	 * Processes the PLAN command
	 */
	public void processPLAN(String name, ArrayList<Location> plan){
		getGhostWithName(name).processPLAN(plan);
	}
	
	/**
	 * Processes the CANCELPLAN command.
	 */
	public void processCANCELPLAN(String name){
		getGhostWithName(name).processCANCELPLAN();
	}
	
	/**
	 * Processes the PING command
	 * @throws IOException 
	 */
	public void processPING(String name, String destination, String message) throws IOException{
		if(destination.equals("*")){
			for(Ghost ghost: getGhosts()){
				ghost.processPING(name, message);
			}
		}else{
			getGhostWithName(destination).processPING(name, message);
		}
		
	}
	
	/**
	 * Processes the PONG command
	 */
	public void processPONG(String destination, String message){
		getGhostWithName(destination).processPONG(message);
	}
	
	/**
	 * Clears all known names.
	 * Used when multiple NAME commands were received with the same name
	 */
	private void resetNames() {
		this.setGhosts(new Ghost[4]);
	}
	
	/**
	 * Returns the player with a given name.
	 * If no player with this name is registered in this game, null is returned
	 */
	private Ghost getGhostWithName(String name){
		Ghost[] ghosts = getGhosts();
		for(int i = 0; i < ghosts.length; i++){
			Ghost ghost = ghosts[i];
			if(ghost != null && ghost.getName().equals(name)) {
				return ghost;
			}
		}
		return null;
	}

	public int getNumberOfJoins() {
		return numberOfJoins;
	}

	private void setNumberOfJoins(int numberOfJoins) {
		this.numberOfJoins = numberOfJoins;
	}

	public int getNumberOfNamesRegistered() {
		return numberOfNamesRegistered;
	}

	private void setNumberOfNamesRegistered(int numberOfNamesRegistered) {
		this.numberOfNamesRegistered = numberOfNamesRegistered;
	}
	

	public ArrayList<Coordinate> getLastPacmanLocations() {
		return lastPacmanLocations;
	}

	private void setLastPacmanLocations(ArrayList<Coordinate> lastPacmanLocations) {
		this.lastPacmanLocations = lastPacmanLocations;
	}
	
	public Channel getChannel() {
		return channel;
	}

	private void setChannel(Channel channel) {
		this.channel = channel;
	}
	
	public CommandParser getParser() {
		return parser;
	}

	private void setParser(CommandParser parser) {
		this.parser = parser;
	}

	public Timer getTimer() {
		return timer;
	}

	private void setTimer(Timer timer) {
		this.timer = timer;
	}

	public Pacman getPacman() {
		return pacman;
	}

	protected void setPacman(Pacman pacman) {
		this.pacman = pacman;
	}
	
	public Ghost[] getGhosts() {
		return ghosts;
	}

	public void setGhosts(Ghost[] ghosts) {
		this.ghosts = ghosts;
		//this method is used to add the other ghosts to hashmap<String,Maze>
		//Comment out when using testFourSimulators
		//setOtherMazes(ghosts);
	}
	/**
	 * This method is used to add the different mazes for 1 robot.
	 * @param ghosts
	 */
	public void setOtherMazes(Ghost[] ghosts){
		for(int i=0; i<ghosts.length;i++){
			Ghost ghost = ghosts[i];
			for(int j=0;j<ghosts.length;j++){
				Ghost otherGhost = ghosts[j];
				if(!otherGhost.getName().equals(ghost.getName()) && otherGhost !=null){
					ghost.addOtherRobotMaze(otherGhost.getName());
					System.out.println("size: "+ ghost.getAllMazes().size());
				}
			}
		}
			
	}
	public QueueingConsumer getConsumer() {
		return consumer;
	}
	
	private void setConsumer(QueueingConsumer consumer) {
		this.consumer = consumer;
	}
//	public int[][] getStartingPositions(){
//		
//	}
	public HashMap<Ghost, Coordinate> getGhostPositions() {
		return ghostPositions;
	}

	private void setGhostPositions(HashMap<Ghost, Coordinate> ghostPositions) {
		this.ghostPositions = ghostPositions;
	}

}